/*
Copyright (C) 2007 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * @fileoverview Functions for handling UI arrangements of a clock collection.
 *     The code used in this file is IE specific.
 */

/**
 * Class for holding and managing all aspects of a collection of clocks.
 * The constructor reads all clocks stored in options and adds them.
 * @constructor
 * @requires CLOCK_INF
 * @requires OPTION_NAMES
 * @requires Clock
 */
function ClockPanel() {
  /**
   * An array holding all clocks in this clock panel.
   * @type Array
   */
  this.clocks = [];

  /**
   * The index of the clock that is currently got the mouse over it.
   * Use -1 if no clock is focused.
   * @type Number
   */
  this.focusedClock = -1;

  /**
   * The clock panel's clock arrangement as follows:
   * 0=horizontal, 1=vertical, 2=two-lines horizontal, 3=two-lines vertical
   * @type Number
   */
  this.clockArrangement = 2;

  /**
   * The clock panel's current zoom level as a percentage.
   * @type Number
   */
  this.curentZoom = 100;

  /**
   * The width of a clock in this clock panel.
   * @type Number
   */
  this.clocksWidth = CLOCK_INF.ORIGINAL_WIDTH;

  /**
   * The height of a clock in this clock panel.
   * @type Number
   */
  this.clocksHeight = CLOCK_INF.ORIGINAL_HEIGHT;

  // read stored value for showing second's handle - default to not show
  if (isNaN(parseInt(this.showSeconds =
      options.getValue(OPTION_NAMES.SHOW_SECONDS)))) {
    options.putValue(OPTION_NAMES.SHOW_SECONDS, 0);
    this.showSeconds = 0;
  }

  /**
   * Boolean stating if the clocks in this clock panel show a second handle.
   * @type Boolean
   */
  this.showSeconds = this.showSeconds == 1;

  // read stored value for showing time tooltip - default to not show
  if (isNaN(parseInt(this.showTooltip =
      options.getValue(OPTION_NAMES.SHOW_TOOLTIP)))) {
    options.putValue(OPTION_NAMES.SHOW_TOOLTIP, 0);
    this.showTooltip = 0;
  }

  /**
   * Boolean stating if the clocks in this clock panel show a tooltip.
   * @type Boolean
   */
  this.showTooltip = (this.showTooltip == 1);

  // read stored value for shrinking when idle - default is shrink
  if (isNaN(parseInt(this.shrinkIfIdle =
      options.getValue(OPTION_NAMES.SHRINK_IF_IDLE)))) {
    options.putValue(OPTION_NAMES.SHRINK_IF_IDLE, 1);
    this.shrinkIfIdle = 1;
  }

  /**
   * Boolean stating if this clock panel shrinks when it is idle.
   * @type Boolean
   */
  this.shrinkIfIdle = (this.shrinkIfIdle == 1);

  // read stored value for the shrinking time - default is 10 seconds
  if (isNaN(parseInt(this.shrinkTime =
      options.getValue(OPTION_NAMES.SHRINK_TIME)))) {
    options.putValue(OPTION_NAMES.SHRINK_TIME, 10);

    /**
     * The time in seconds before a this panel is considered idle.
     * @type Number
     */
    this.shrinkTime = 10;
  }

  // Read stored value for the shrinking percent (default is 50%).
  if (isNaN(parseInt(this.shrinkPercent =
      options.getValue(OPTION_NAMES.SHRINK_PERCENT)))) {
    options.putValue(OPTION_NAMES.SHRINK_PERCENT, 50);

    /**
     * The percentage to which this panel will zoom if idle.
     * @type Number
     */
    this.shrinkPercent = 50;
  }

  // Read the clocks stored from last use and create objects for all of them.
  this.clocks.push(new Clock(view,
      this.getClockOption(0, OPTION_NAMES.NUMBERED_DESIGN, false, 0,
          CLOCK_INF.DESIGNS.length - 1, 0),
      this.getClockOption(0, OPTION_NAMES.NUMBERED_BACKGROUND, false, 0,
          CLOCK_INF.BACKGROUND_NAMES.length - 1, 0),
      this.getClockOption(0, OPTION_NAMES.NUMBERED_OFFSET, false,
          NaN, NaN, 0),
      this.getClockOption(0, OPTION_NAMES.NUMBERED_LOCATION, true, 0, 0, ''),
      this.showSeconds, true));
  this.clocks[0].eventArea.onmouseover = (function(refThis) {
    return function () {
      refThis.focusedClock = 0;
    };
  })(this);
  this.clocks[0].eventArea.onmouseout = (function(refThis) {
    return function() {
      refThis.focusedClock = -1;
    };
  })(this);
  for (var i = 1; i < MAX_CLOCKS; ++i) {
    if (options.exists(OPTION_NAMES.NUMBERED_PREFIX + i +
        OPTION_NAMES.NUMBERED_OFFSET)) {
      this.clocks.push(new Clock(view,
          this.getClockOption(i, OPTION_NAMES.NUMBERED_DESIGN, false, 0,
              CLOCK_INF.DESIGNS.length - 1, 0),
          this.getClockOption(i, OPTION_NAMES.NUMBERED_BACKGROUND, false, 0,
              CLOCK_INF.BACKGROUND_NAMES.length - 1, 0),
          this.getClockOption(i, OPTION_NAMES.NUMBERED_OFFSET, false,
              NaN, NaN, 0),
          this.getClockOption(i, OPTION_NAMES.NUMBERED_LOCATION, true,
              0, 0, ''),
          this.showSeconds, true));
      this.clocks[i].eventArea.onmouseover = (function(i, refThis) {
        return function() {
          refThis.focusedClock = i;
        };
      })(i, this);
      this.clocks[i].eventArea.onmouseout = (function(refThis) {
        return function() {
          refThis.focusedClock = -1;
        };
      })(this);
    }
  }
  options.putValue(OPTION_NAMES.TOTAL_CLOCKS, this.clocks.length);

  // Read the stored clock arrangement and use 2 line horizontal if failed.
  if (isNaN(parseInt(this.clockArrangement =
      options.getValue(OPTION_NAMES.CLOCK_ARRANGEMENT)))) {
    options.putValue(OPTION_NAMES.CLOCK_ARRANGEMENT, 2);
    this.clockArrangement = 2;
  }
  if (this.clockArrangement < 0 || this.clockArrangement > 5) {
    this.clockArrangement = 2;
  }

  // If there are to few clocks for the stored arrangement change it:
  while ((this.clockArrangement > 1 && this.clocks.length < 3) ||
         (this.clockArrangement > 3 && this.clocks.length < 9)) {
    this.clockArrangement -= 2;
    options.putValue(OPTION_NAMES.CLOCK_ARRANGEMENT, this.clockArrangement);
  }

  this.zoom(this.curentZoom);

  /**
   * Stores a token to an interval that updates the second handles of
   * all clocks.
   * @type Number
   */
  this.secondInterval = null;

  /**
   * Stores a token to an interval that updates the time of all clocks.
   * @type Number
   */
  this.minuteTimeout = null;

  /**
   * Stores a token to an animation interval used for zooming.
   * @type Number
   */
  this.zoomTimer = null;
}

/**
 * The method arranges all clocks and sets the width and the height of the main
 * view. It takes into account both the zoom and the current layout chosen.
 * @requires CLOCK_INF
 * @requires Clock
 */
ClockPanel.prototype.arrangeClocks = function() {
  /**
   * Counter variable to be used in loops.
   * @type Number
   */
  var i = 0;

  // If we have a horizontal arrangement we enter the if:
  if (this.clockArrangement % 2 === 0) {

    /**
     * The number of clock rows we need to render.
     * @type Number
     */
    var rows = this.clockArrangement / 2 + 1;  // find how many rows there are

    /**
     * Number of clocks to be drawn on the first row.
     * @type Number
     */
    var firstRowUsage = Math.ceil(this.clocks.length / rows);

    /**
     * Number of clocks still left to be drawn.
     * @type Number
     */
    var clocksLeft = this.clocks.length;

    while (rows > 0) {
      /**
       * The indentation of the current row in pixels from the left.
       * @type Number
       */
      var indent = Math.ceil(clocksLeft/rows) < firstRowUsage ?
          Math.floor((this.clocksWidth + CLOCK_INF.HORIZONTAL_MARGIN) /
              2) : 0;
      for (var j = 0; j < clocksLeft / rows; ++j) {
        this.clocks[i].moveTo((this.clocksWidth +
            CLOCK_INF.HORIZONTAL_MARGIN) * j + indent,
            (this.clocksHeight + CLOCK_INF.VERTICAL_MARGIN) *
                (this.clockArrangement / 2 + 1 - rows));
        ++i;
      }
      clocksLeft -= Math.ceil(clocksLeft / rows);
      --rows;
    }

    // The view width and height are set only at the end to allow all clocks to
    // be visible when resizing even if it means a reinit of rows.
    rows = this.clockArrangement / 2 + 1;
    view.width = (this.clocksWidth + CLOCK_INF.HORIZONTAL_MARGIN) *
                 Math.ceil(this.clocks.length / rows) -
                 CLOCK_INF.HORIZONTAL_MARGIN;
    view.height = (this.clocksHeight + CLOCK_INF.VERTICAL_MARGIN) * rows -
                  CLOCK_INF.VERTICAL_MARGIN;

  } else if (this.clockArrangement % 2 == 1) {
    // Otherwise it's a vertical arrangement.

    /**
     * The number of clock columns we need to render.
     * @type Number
     */
    var columns = (this.clockArrangement + 1) / 2;
    for (; i < this.clocks.length; ++i) {
      this.clocks[i].moveTo((this.clocksWidth + CLOCK_INF.HORIZONTAL_MARGIN) *
          (i % columns), (this.clocksHeight + CLOCK_INF.VERTICAL_MARGIN) *
              Math.floor(i / columns));
    }
    view.width = (this.clocksWidth + CLOCK_INF.HORIZONTAL_MARGIN) * columns -
                 CLOCK_INF.HORIZONTAL_MARGIN;
    view.height = (this.clocksHeight + CLOCK_INF.VERTICAL_MARGIN) *
                  Math.ceil(this.clocks.length / columns) -
                  CLOCK_INF.HORIZONTAL_MARGIN;
  }
};

/**
 * This function sets the exact time of each clock and then
 * sets itself a timer for 30 seconds.
 * It also compensates any errors that the {@link #onSecondTimer} might
 * have caused.
 * @requires Clock
 */
ClockPanel.prototype.onMinuteTimer = function() {
  /**
   * The time and date the computer's clock is currently showing.
   * @type Date
   */
  var now = new Date();
  for (var i = 0; i < this.clocks.length; ++i) {
    this.clocks[i].setTime(now, this.showTooltip);
  }
  this.minuteTimeout = setTimeout((function(refThis) {
    return function() {
      refThis.onMinuteTimer();
    };
  })(this), 30000);
};

/**
 * When seconds are active this function gets called every second.
 * It calls the updateSecondHand of each clock which will cause a second
 * to pass without checking the actual real time.
 * All corrections are made by {@link #onMinuteTimer}.
 * @requires Clock
 */
ClockPanel.prototype.onSecondTimer = function() {
  for (var i = 0; i < this.clocks.length; ++i) {
    this.clocks[i].updateSecondHand();
  }
};

/**
 * Shows or hides all seconds handles.
 * @param {Boolean} show True indicates the seconds handles are to be shown.
 * @requires Clock
 */
ClockPanel.prototype.toggleSecondsHandles = function(show) {
  for (var i = 0; i < this.clocks.length; ++i) {
    if (this.clocks[i].secondHand_ !== null) {
      this.clocks[i].secondHand_.visible = show;
    }
  }
  if (show) {
    // if we are now showing the seconds we start an interval to update them
    if (this.secondInterval !== null) {
      clearInterval(this.secondInterval);
    }
    this.secondInterval = setInterval((function(refThis) {
      return function() {
        refThis.onSecondTimer();
      };
    })(this), 1000);
    clearInterval(this.minuteTimeout);
    // set the exact time on each clock to sink up the added seconds handles
    this.onMinuteTimer();
  } else if (this.secondInterval !== null) {
    clearInterval(this.secondInterval);
    this.secondInterval = null;
  }
};

/**
 * Hides all clock tooltips. Called when the option to show tooltips
 * is deselected.
 * @requires Clock
 */
ClockPanel.prototype.hideTooltips = function() {
  for (var i = 0; i < this.clocks.length; ++i) {
    this.clocks[i].eventArea.tooltip = '\x00';
  }
};

/**
 * Sets this.clocksWidth and this.clocksHeight to the indicated percent of their
 * original size and calls the {@link Clock#zoom} function of all clocks.
 * @param {Number} percent The percentage to which the clocks will zoom.
 * @requires Clock
 */
ClockPanel.prototype.zoom = function(percent) {
  this.curentZoom = percent;
  this.clocksWidth = Math.round(percent * CLOCK_INF.ORIGINAL_WIDTH / 100);
  this.clocksHeight = Math.round(percent * CLOCK_INF.ORIGINAL_HEIGHT / 100);
  // zoom each clock to fit the this.clocksWidth * this.clocksHeight rectangle
  for (var i = 0; i < this.clocks.length; ++i) {
    this.clocks[i].zoom(this.clocksWidth, this.clocksHeight);
  }
  this.arrangeClocks();
};

/**
 * Creates a zooming animation by calling the {@link ClockPanel#zoom} function.
 * @param {Number} startPercent Sets the initial zoom (percentual)
 *     of the clocks for the animation.
 * @param {Number} stopPercent It is the final zoom (percentual)
 *     of the clocks when done with zooming.
 * @param {Number} stepSize Percentual increase on each step
 *     (must be negative for shrinking animations).
 * @param {Number} stepTime The delay in ms between steps.
 */
ClockPanel.prototype.animateZoom = function(startPercent, stopPercent,
                                            stepSize, stepTime) {
  if ((startPercent >= stopPercent && stepSize > 0) ||
      (startPercent <= stopPercent && stepSize < 0) || stepSize === 0) {
    this.zoom(stopPercent);
    this.zoomTimer = null;
  } else {
    this.zoom(startPercent);
    this.zoomTimer = setTimeout((function(refThis) {
      return function() {
        refThis.animateZoom(startPercent + stepSize, stopPercent, stepSize,
            stepTime);
      };
    })(this), stepTime);
  }
};

/**
 * Function used to fetch an option of a clock
 * @param {Number} clockNum The number of the clock whose option is needed
 * @param {String} optionType The option name of the required option. Must be
 *     one of the following : OPTION_NAMES.NUMBERED_OFFSET,
 *     OPTION_NAMES.NUMBERED_DESIGN, OPTION_NAMES.NUMBERED_BACKGROUND,
 *     OPTION_NAMES.NUMBERED_LOCATION.
 * @param {Boolean} isString If true, the option is treated like a string,
 *     otherwise it's considered an integer.
 * @param {Number} minValue Only used if isString is false.
 *     The minimum allowed value for the option. If a smaler number is
 *     stored in the option, defaultValue is returned.
 * @param {Number} maxValue Only used if isString is false.
 *     The maximum allowed value for the option. If a larger number is
 *     stored in the option, defaultValue is returned.
 * @param {Number|String} defaultValue Value to be returned if fetching the
 *     option failed or if isString is false and the option is not an integer
 *     or not in the allowed interval.
 *     If this value is returned, the function will also overwrite the option.
 * @returns the value stored in the option as an integer or a string,
 *     or defaultValue.
 */
ClockPanel.prototype.getClockOption = function(clockNum, optionType, isString,
    minValue, maxValue, defaultValue) {
  var result = options.getValue(OPTION_NAMES.NUMBERED_PREFIX +
      clockNum + optionType);
  if (!isString) {
    result = parseInt(result);
    if (isNaN(result) || result < minValue || result > maxValue) {
      options.putValue(OPTION_NAMES.NUMBERED_PREFIX + clockNum + optionType,
          defaultValue);
      return defaultValue;
    }
  }
  return result;
};

/**
 * Function used by {@link Array#sort} function to sort an array of
 * {@link Clock} instances by their offset.
 * @param {Clock} x One of the items in the array to be compared.
 * @param {Clock} y The other item in the array to be compared.
 * @returns an integer : 1 if x > y , -1 if x < y and 0 if x = 0.
 * @requires Clock
 */
ClockPanel.sortByOffset = function(x, y) {
  return x.offsetMinutes < y.offsetMinutes ? -1 :
      x.offsetMinutes > y.offsetMinutes ? 1 : 0;
};
